// 
//  Copyright © 2009 Jiří Zárevúcky <zarevucky.jiri@gmail.com>
// 
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU Affero General Public License as
//  published by the Free Software Foundation, either version 3 of the
//  License, or (at your option) any later version.
// 
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU Affero General Public License for more details.
// 
//  You should have received a copy of the GNU Affero General Public License
//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
// 
// 



using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Net.Security;
using System.Collections.Generic;
using System.Threading;
using System.Security.Cryptography.X509Certificates;

using Anculus.Core;

using System.IO;

using Galaxium.Protocol.Xmpp.Library.Utility;

// TODO: handle TTL of DNS records

namespace Galaxium.Protocol.Xmpp.Library.Core
{	
	public sealed class Connection
	{
		public const int DEFAULT_PORT = 5222;

		private Socket _socket;
		private Stream _stream;
		
		private RemoteCertificateValidationCallback _callback;
		
		public JabberID UID { get; private set; }

		public int KeepaliveInterval  { get; set; }
		
		public bool Connected {
			get {
				if (_socket != null && !SocketIsConnected (_socket))
					Disconnect ();
				return _socket != null;
			}
		}
		
		public string CustomDomain { get; private set; }
		public int?   CustomPort   { get; private set; }

		public Connection (JabberID uid, RemoteCertificateValidationCallback callback)
			:this (uid, null, null, callback)
		{
		}
		
		public Connection (JabberID uid, string domain, int? port,
		                   RemoteCertificateValidationCallback callback)
		{
			if (uid == null)
				throw new ArgumentNullException ("uid");
			if (callback == null)
				throw new ArgumentNullException ("callback");
			
			UID = uid;
			CustomDomain = domain;
			CustomPort = port;
			KeepaliveInterval = 60000;
			_callback = callback;
		}
		
		private static bool SocketIsConnected (Socket socket)
		{
			if (socket == null) return false;
			
			// Socket.Connected doesn't tell us if the socket is actually connected...
			// http://msdn2.microsoft.com/en-us/library/system.net.sockets.socket.connected.aspx
			
			if (!socket.Connected) return false;
			
			bool blocking = socket.Blocking;
			try {
				socket.Blocking = false;
				socket.Send (new byte[0], 0, 0);
				return true;
			}
			catch (SocketException ex) {
				if (ex.SocketErrorCode == SocketError.WouldBlock) return true;
				throw;
			}
			finally {
				socket.Blocking = blocking;
			}
		}
		
		private bool TryConnect () // throws nothing
		{
			if (CustomDomain != null) {
				Log.Warn ("Connection host and port overridden.");
				var record = new SrvRecord (CustomDomain, CustomPort ?? DEFAULT_PORT, 0, 0);
				return TryConnect (record);
			}
			
			SrvRecord[] records = SrvResolver.Resolve ("_xmpp-client._tcp." + UID.Domain);
			
			foreach (var record in records) {
				if (_socket == null) return false;
				if (TryConnect (record)) return true;
			}
			
			return TryConnect (new SrvRecord (UID.Domain, DEFAULT_PORT, 0, 0));
		}
		
		private bool TryConnect (SrvRecord record) // throws nothing
		{
			Log.Info ("Trying to connect to domain " + record.Name + ", port " + record.Port);
			
			var host = Dns.GetHostEntry (record.Name);
			
			foreach (var ip in host.AddressList)
				Log.Debug ("Resolved IP: " + ip.ToString ());
			
			foreach (var ip in host.AddressList) {
				Log.Info ("Connecting to IP " + ip.ToString ());
				try { _socket.Connect (ip, record.Port); }
				catch (Exception e) {
					Log.Error (e, "Exception while trying to connect.");
				}
				if (SocketIsConnected (_socket)) {
					Log.Info ("Connection succeeded.");
					return true;
				} else Log.Warn ("Connection failed.");
				if (_socket == null)
					return false;
			}
			
			return false;
		}
		
		public void Connect ()  // throws InvalidOperationException, ConnectionException
		{
			if (Connected)
				throw new InvalidOperationException ("Already connected.");
			
			_socket = new Socket (AddressFamily.InterNetwork,
			                      SocketType.Stream, ProtocolType.Tcp);
			
			if (!TryConnect ()) {
				_socket = null;
				throw new ConnectionException ("Unable to connect.");
			}

			_stream = new NetworkStream (_socket, true);
		}
		
		public void Disconnect ()  // throws nothing
		{
			if (_stream != null) {
				_stream.Close ();
				_stream = null;
			}
			if (_socket != null) {
				_socket.Close ();
				_socket = null;
			}
		}

		public void EnableTLS () // throws InvalidOperationException, AuthenticationException
		{
			if (!Connected)
				throw new InvalidOperationException ();
			
			_stream = new SslStream (_stream, false, _callback);
			(_stream as SslStream).AuthenticateAsClient (UID.Domain);
		}

		public void InitKeepAlive () // throws nothing
		{
			var thread = new Thread (() => {
				while (true) {
					Thread.Sleep (KeepaliveInterval);
					try { Send (" "); }
					catch { break; }
				}
			});
			thread.IsBackground = true;
			thread.Start ();
		}
		
		private const int BUFFER_SIZE = 1024;
		
		private byte[] _buffer = new byte [BUFFER_SIZE];
		
		public string Receive () // throws InvalidOperationException
			// returns empty string when disconnected while reading
		{
			if (!Connected)
				throw new InvalidOperationException ("Connection isn't connected.");
			
			var count = _stream.Read (_buffer, 0, BUFFER_SIZE);
			return Encoding.UTF8.GetString (_buffer, 0, count);
			
			// FIXME: check if there is a complete UTF-8 character at the end
		}

		public void Send (string data) // throws InvalidOperationException
		{
			if (data == null)
				throw new ArgumentNullException ("data");
			
			Log.Debug (data);
			
			lock (_stream) {
				try {
					if (!Connected)
						throw new InvalidOperationException ("Connection isn't connected.");
					byte[] bytes = Encoding.UTF8.GetBytes (data);
					_stream.Write (bytes, 0, bytes.Length);
				}
				catch (Exception e) {
					Log.Error (e, "Exception occurred while sending");
				}
			}
		}
	}
}